#include "compute.thread.h"

#include<QtWidgets/QtWidgets>

#include <deque>
#include <mutex>
#include <condition_variable>

#include "tracer.h"
#include "session.h"
#include "plugins/plugin.h"
#include "misc.h"
#include "options.h"
#include "options.page.h"
#include "load.thread.h"
#include "plugin.manager.h"



#include "compute.thread.options.h"

struct ComputeThreadPriv {
	Session* session;
	ComputeThread* thread;

	int update_interval;
	qint64 prev_update;


	std::mutex mtx_input;
	std::mutex mtx_update;
	std::mutex mtx_output;
	std::condition_variable cv_input;

	AlgFind* algFind;
	AlgConn* algConn;
	AlgRefine* algRefine;
	AlgRefine* algRefineAuto;

	bool stopRequested;
	bool cancelCompute;
	bool position_changed;
	std::deque<AlgType> que_input;
	Position input_p0;
	Position input_p1;
	Graph input_gr;
	Path input_pth;

	AlgType output_type;
	Path output_pth;

	Cube cubes[3];
	CubeRef cuberefs[3];
	std::pair<double, double> xfuncs[3];

	std::deque<Cube> que_cubes[3];
	std::deque<std::pair<double, double>> que_xfuncs[3];

	std::vector<int32_t> update_data;

	bool getPath(Path* path, bool* connected) {
		std::unique_lock<std::mutex> lck{mtx_output};
		switch(output_type) {
			case ALG_FIND:
				if(output_pth.points.size()<2)
					return false;
				std::swap(*path, output_pth);
				*connected=false;
				return true;
			case ALG_CONN:
				if(output_pth.points.size()<2)
					return false;
				std::swap(*path, output_pth);
				*connected=true;
				return true;
			case ALG_REFINE:
				if(output_pth.points.size()<2)
					return false;
				std::swap(*path, output_pth);
				return true;
			default:
				throwError("Unexpected type.");
		}
		return false;
	}
	void run() {
		std::unique_lock<std::mutex> lck_input{mtx_input};
		while(!stopRequested) {
			if(position_changed) {
				position_changed=false;
				lck_input.unlock();
				for(auto alg: std::array<Algorithm*, 4>{algFind, algConn, algRefine, algRefineAuto})
					alg->updatePosition();
				lck_input.lock();
				continue;
			}
			if(!que_cubes[0].empty()) {
				cubes[0]=std::move(que_cubes[0].back());
				que_cubes[0].clear();
				if(!que_xfuncs[0].empty()) {
					xfuncs[0]=que_xfuncs[0].back();
					que_xfuncs[0].clear();
				}
				lck_input.unlock();
				setupCubeRef<0>();
				lck_input.lock();
				continue;
			}
			if(!que_cubes[1].empty()) {
				cubes[1]=std::move(que_cubes[1].back());
				que_cubes[1].clear();
				if(!que_xfuncs[1].empty()) {
					xfuncs[1]=que_xfuncs[1].back();
					que_xfuncs[1].clear();
				}
				lck_input.unlock();
				setupCubeRef<1>();
				lck_input.lock();
				continue;
			}
			if(!que_cubes[2].empty()) {
				cubes[2]=std::move(que_cubes[2].back());
				que_cubes[2].clear();
				if(!que_xfuncs[2].empty()) {
					xfuncs[2]=que_xfuncs[2].back();
					que_xfuncs[2].clear();
				}
				lck_input.unlock();
				setupCubeRef<2>();
				lck_input.lock();
				continue;
			}
			if(!que_xfuncs[0].empty()) {
				xfuncs[0]=que_xfuncs[0].back();
				que_xfuncs[0].clear();
				lck_input.unlock();
				setupXfunc<0>();
				lck_input.lock();
				continue;
			}
			if(!que_xfuncs[1].empty()) {
				xfuncs[1]=que_xfuncs[1].back();
				que_xfuncs[1].clear();
				lck_input.unlock();
				setupXfunc<1>();
				lck_input.lock();
				continue;
			}
			if(!que_xfuncs[2].empty()) {
				xfuncs[2]=que_xfuncs[2].back();
				que_xfuncs[2].clear();
				lck_input.unlock();
				setupXfunc<2>();
				lck_input.lock();
				continue;
			}

			if(!que_input.empty()) {
				auto input_type=que_input.back();
				que_input.clear();
				auto p0=input_p0;
				auto p1=input_p1;
				auto gr=input_gr;
				auto pth=input_pth;
				lck_input.unlock();

				prev_update=QDateTime::currentMSecsSinceEpoch();

				switch(input_type) {
					case ALG_FIND:
						{
							std::unique_lock<std::mutex> lck_out{mtx_output};
							output_type=input_type;
							algFind->find(p0, gr, &output_pth);
							if(output_pth.points.size()>=2) {
								algRefineAuto->refine(gr, &output_pth);
							}
						}
						break;
					case ALG_CONN:
						{
							std::unique_lock<std::mutex> lck_out{mtx_output};
							output_type=input_type;
							algConn->connect(p0, p1, gr, &output_pth);
							if(output_pth.points.size()>=2) {
								algRefineAuto->refine(gr, &output_pth);
							}
						}
						break;
					case ALG_REFINE:
						{
							std::unique_lock<std::mutex> lck_out{mtx_output};
							output_type=input_type;
							std::swap(pth, output_pth);
							algRefine->refine(gr, &output_pth);
						}
						break;
					default:
						throwError("Wrong type of computation.");
				}

				Q_EMIT thread->computeFinished();
				lck_input.lock();
				continue;
			}
			cv_input.wait(lck_input);
		}
	}

	void stop() {
		std::unique_lock<std::mutex> lck{mtx_input};
		stopRequested=true;
		cancelCompute=true;
		cv_input.notify_one();
	}
	ComputeThreadPriv(Session* s, ComputeThread* _t);
	~ComputeThreadPriv();

	template<int I> void setupCubeRef();
	template<int I> void setupXfunc();
	void updateCube(int ch, const Cube& cube, const std::pair<double, double>& xfunc) {
		std::unique_lock<std::mutex> lck_input{mtx_input};
		que_cubes[ch].push_back(cube);
		que_xfuncs[ch].push_back(xfunc);
	}
	void updateXfunc(int ch, const std::pair<double, double>& xfunc) {
		std::unique_lock<std::mutex> lck_input{mtx_input};
		que_xfuncs[ch].push_back(xfunc);
	}
	void positionChanged() {
		std::unique_lock<std::mutex> lck_input{mtx_input};
		position_changed=true;
	}

	void find(const Position& p, Graph gr) {
		Tracer::instance()->logMessage("STAT: ", "FIND: ", algFind->info()->name);
		std::unique_lock<std::mutex> lck{mtx_input};
		que_input.push_back(ALG_FIND);
		input_p0=p;
		input_gr=gr;
		cv_input.notify_one();
	}
	void connect(const Position& p0, const Position& p1, Graph gr) {
		Tracer::instance()->logMessage("STAT: ", "CONN: ", algConn->info()->name);
		std::unique_lock<std::mutex> lck{mtx_input};
		que_input.push_back(ALG_CONN);
		input_p0=p0;
		input_p1=p1;
		input_gr=gr;
		cv_input.notify_one();
	}
	void refine(const Path& pth, Graph gr) {
		Tracer::instance()->logMessage("STAT: ", "REFINE: ", algRefine->info()->name);
		std::unique_lock<std::mutex> lck{mtx_input};
		que_input.push_back(ALG_REFINE);
		input_pth=pth;
		input_gr=gr;
		cv_input.notify_one();
	}
	void cancel() {
		Tracer::instance()->logMessage("STAT: ", "ABORT");
		std::unique_lock<std::mutex> lck{mtx_input};
		cancelCompute=true;
	}
};

static bool sync_callback(Algorithm* alg, void* p) {
	auto tp=static_cast<ComputeThreadPriv*>(p);
	{
		std::unique_lock<std::mutex> lck_input{tp->mtx_input};
		if(tp->cancelCompute) {
			tp->cancelCompute=false;
			return false;
		}
	}
	if(tp->update_interval==0)
		return true;
	auto now=QDateTime::currentMSecsSinceEpoch();
	auto dur=now-tp->prev_update;
	if(dur<tp->update_interval)
		return true;

	std::unique_lock<std::mutex> lck_update{tp->mtx_update, std::try_to_lock_t{}};
	if(lck_update) {
		auto dat=alg->getUpdate();
		std::swap(tp->update_data, dat);
		tp->prev_update=now;
		Q_EMIT tp->thread->showProgress();
	}
	return true;
}


template<int I>
void ComputeThreadPriv::setupCubeRef() {
	auto& cuberef=cuberefs[I];
	auto& cube=cubes[I];
	cuberef.data=nullptr;

	if(cube.data) {
		switch(cube.data.type()) {
			case CubeType::U8:
				cuberef.dataU8=reinterpret_cast<const uint8_t*>(cube.data.data());
				cuberef.is16bit=false;
				break;
			case CubeType::U16:
				cuberef.dataU16=reinterpret_cast<const uint16_t*>(cube.data.data());
				cuberef.is16bit=true;
				break;
			default:
				cuberef.data=nullptr;
		}
	}
	if(cuberef.data) {
		cuberef.width=cube.data.width();
		cuberef.height=cube.data.height();
		cuberef.depth=cube.data.depth();

		cuberef.widthAdj=cube.data.widthAdj();
		cuberef.heightAdj=cube.data.heightAdj();
		cuberef.depthAdj=cube.data.depthAdj();

		cuberef.xres=cube.xform.resolution[0];
		cuberef.yres=cube.xform.resolution[1];
		cuberef.zres=cube.xform.resolution[2];

		for(int i=0; i<3; i++)
			cuberef.origin[i]=cube.xform.origin[i]+cube.xform.direction[i]*cube.data.cubeId().x0+cube.xform.direction[i+3]*cube.data.cubeId().y0+cube.xform.direction[i+6]*cube.data.cubeId().z0;
		for(int i=0; i<9; i++)
			cuberef.dir[i]=cube.xform.direction[i];
		for(int i=0; i<9; i++)
			cuberef.rdir[i]=cube.xform.direction_inv[i];
	}

	for(auto alg: std::array<Algorithm*, 4>{algFind, algConn, algRefine, algRefineAuto})
		alg->updateCube(I, cuberef, xfuncs[I]);
}
template<int I>
void ComputeThreadPriv::setupXfunc() {
	for(auto alg: std::array<Algorithm*, 4>{algFind, algConn, algRefine, algRefineAuto})
		alg->updateXfunc(I, xfuncs[I]);
}
ComputeThreadPriv::ComputeThreadPriv(Session* s, ComputeThread* _t):
	session{s}, thread{_t},
	mtx_input{}, mtx_update{},
	algFind{nullptr}, algConn{nullptr}, algRefine{nullptr}, algRefineAuto{nullptr},
	stopRequested{false}, cancelCompute{false}
{
	auto options=session->options();
	ComputeGeneralOptions::instance()->config(this, options);
	ComputeAlgOptions<ALG_FIND>::instance()->config(this, options);
	ComputeAlgOptions<ALG_CONN>::instance()->config(this, options);
	ComputeAlgOptions<ALG_REFINE>::instance()->config(this, options);
	ComputeAlgOptions<ALG_REFINE_AUTO>::instance()->config(this, options);
}
ComputeThreadPriv::~ComputeThreadPriv() {
	//opt_page_compute_general->detach();
	//opt_page_compute_find->detach();
	//opt_page_compute_conn->detach();
	//opt_page_compute_refine->detach();
	//opt_page_compute_refine_auto->detach();
}

ComputeThread::ComputeThread(Session* s):
	QThread{s}, priv{new ComputeThreadPriv{s, this}}
{
}
ComputeThread::~ComputeThread() {
	delete priv;
}

void ComputeThread::stop() {
	priv->stop();
}

void ComputeThread::find(const Position& p, Graph gr) {
	priv->find(p, gr);
}
void ComputeThread::connect(const Position& p0, const Position& p1, Graph gr) {
	priv->connect(p0, p1, gr);
}
void ComputeThread::refine(const Path& pth, Graph gr) {
	priv->refine(pth, gr);
}

const std::vector<int32_t>& ComputeThread::progressData() const {
	priv->mtx_update.lock();
	return priv->update_data;
}
void ComputeThread::progressDataUpdated() {
	priv->mtx_update.unlock();
}


void ComputeThread::run() {
	printMessage("Compute Thread started");
	try {
		priv->run();
	} catch(const std::exception& e) {
		Q_EMIT threadError(QString{"Unexpected error in cache thread: "}+=e.what());
		printMessage("Compute thread error");
	}
	printMessage("Compute Thread finished");
}
bool ComputeThread::getPath(Path* path, bool* connected) {
	return priv->getPath(path, connected);
}

void ComputeThread::updateCube(int ch, const Cube& cube, const std::pair<double, double>& xfunc) {
	priv->updateCube(ch, cube, xfunc);
}
void ComputeThread::updateXfunc(int ch, const std::pair<double, double>& xfunc) {
	priv->updateXfunc(ch, xfunc);
}
void ComputeThread::positionChanged() {
	priv->positionChanged();
}
void ComputeThread::saveOptions(Options* opt) const {
	ComputeGeneralOptions::instance()->saveConfig(priv, opt);
	ComputeAlgOptions<ALG_FIND>::instance()->saveConfig(priv, opt);
	ComputeAlgOptions<ALG_CONN>::instance()->saveConfig(priv, opt);
	ComputeAlgOptions<ALG_REFINE>::instance()->saveConfig(priv, opt);
	ComputeAlgOptions<ALG_REFINE_AUTO>::instance()->saveConfig(priv, opt);
}
void ComputeThread::cancel() {
	priv->cancel();
}

void ComputeThread::attachOptionsDlg() {
	ComputeGeneralOptions::instance()->attach(priv);
	ComputeAlgOptions<ALG_FIND>::instance()->attach(priv);
	ComputeAlgOptions<ALG_CONN>::instance()->attach(priv);
	ComputeAlgOptions<ALG_REFINE>::instance()->attach(priv);
	ComputeAlgOptions<ALG_REFINE_AUTO>::instance()->attach(priv);
}
void ComputeThread::detachOptionsDlg() {
	ComputeGeneralOptions::instance()->attach(nullptr);
	ComputeAlgOptions<ALG_FIND>::instance()->attach(nullptr);
	ComputeAlgOptions<ALG_CONN>::instance()->attach(nullptr);
	ComputeAlgOptions<ALG_REFINE>::instance()->attach(nullptr);
	ComputeAlgOptions<ALG_REFINE_AUTO>::instance()->attach(nullptr);
}

#include "compute.thread.options.cc"

